module EMV
    TAGS = {
      0x4F => '[ 4F ]Application Identifier (AID)',
      0x50 => '[ 50 ]Application Label',
      0x57 => '[ 57 ]Track 2 Equivalent Data',
      0x5A => '[ 5A ]Application Primary Account Number(PAN)',
    0x5F20 => '[5F20]Cardholder Name',
    0x5F24 => '[5F24]Application Expiration Date (YYMMDD)',
    0x5F25 => '[5F25]Application Effective Date (YYMMDD)',
    0x5F28 => '[5F28]Issuer Country Code',
    0x5F2A => '[5F2A]Transaction Currency Code',
    0x5F2D => '[5F2D]Language Preference',
    0x5F30 => '[5F30]Service Code',
    0x5F34 => '[5F34]Application PAN Sequence Number',
    0x5F36 => '[5F36]Transaction Currency Exponent',
    0x5F50 => '[5F50]Issuer URL',
    0x5F53 => '[5F53]International Bank Account Number (IBAN)',
    0x5F54 => '[5F34]Bank Identifier Code (BIC)',
    0x5F55 => '[5F55]Issuer Country Code (alpha2 format)',
    0x5F56 => '[5F56]Issuer Country Code (alpha3 format)',
      0x6F => '[ 6F ]File Control Information (FCI) Template',
      0x82 => '[ 82 ]Application Interchange Profile',
      0x87 => '[ 87 ]Application Priority Indicator',
      0x88 => '[ 88 ]Short File Identifier (SFI)',
      0x8A => '[ 8A ]Authorisation Response Code',
      0x8C => '[ 8C ]Card Risk Management Data Object List 1 (CDOL1)',
      0x8D => '[ 8D ]Card Risk Management Data Object List 2 (CDOL2)',
      0x8E => '[ 8E ]Cardholder Verification Method (CVM) List',
      0x8F => '[ 8F ]Certification Authority Public Key Index',
      0x90 => '[ 90 ]Issuer Public Key Certificate',
      0x92 => '[ 92 ]Issuer Public Key Remainder',
      0x93 => '[ 93 ]Signed Static Application Data',
      0x94 => '[ 94 ]Application File Locator (AFL)',
      0x95 => '[ 95 ]Terminal Verification Results',
      0x9A => '[ 9A ]Transaction Date',
      0x9C => '[ 9C ]Transaction Type',
    0x9F01 => '[9F01]Acquirer Identifier',
    0x9F02 => '[9F02]Amount, Authorised (Numeric)',
    0x9F03 => '[9F03]Amount, Cashback (Numeric)',
    0x9F07 => '[9F07]Application Usage Control',
    0x9F08 => '[9F08]Application Version Number',
    0x9F0B => '[9F0B]Cardholder Name Extended',
    0x9F0D => '[9F0D]Issuer Action Code - Default',
    0x9F0E => '[9F0E]Issuer Action Code - Denial',
    0x9F0F => '[9F0F]Issuer Action Code - Online',
    0x9F10 => '[9F10]Issuer Application Data',
    0x9F11 => '[9F11]Issuer Code Table Index',
    0x9F12 => '[9F12]Application Preferred Name',
    0x9F13 => '[9F13]Last Online Application Transaction Counter (ATC) Register',
    0x9F14 => '[9F14]Lower Consecutive Offline Limit',
    0x9F17 => '[9F17]Personal Identification Number (PIN) Try Counter',
    0x9F18 => '[9F18]Issuer Script Identifier',
    0x9F1A => '[9F1A]Terminal Country Code',
    0x9F1B => '[9F1B]Terminal Floor Limit',
    0x9F1C => '[9F1C]Terminal Identification',
    0x9F1F => '[9F1F]Track 1 Discretionary Data',
    0x9F21 => '[9F21]Transaction Time',
    0x9F23 => '[9F23]Upper Consecutive Offline Limit',
    0x9F26 => '[9F26]Application Cryptogram',
    0x9F27 => '[9F27]Cryptogram Information Data',
    0x9F32 => '[9F32]Issuer Public Key Exponent',
    0x9F33 => '[9F33]Terminal Capabilities',
    0x9F34 => '[9F34]Cardholder Verification Method (CVM) Results',
    0x9F35 => '[9F35]Terminal Type',
    0x9F36 => '[9F36]Application Transaction Counter (ATC)',
    0x9F37 => '[9F37]Unpredictable Number',
    0x9F38 => '[9F38]Processing Options Data Object List (PDOL)',
    0x9F42 => '[9F42]Application Currency Code',
    0x9F43 => '[9F43]Application Reference Currency Exponent',
    0x9F44 => '[9F44]Application Currency Exponent',
    0x9F45 => '[9F45]Data Authentication Code',
    0x9F46 => '[9F46]ICC Public Key Certificate',
    0x9F47 => '[9F47]ICC Public Key Exponent',
    0x9F48 => '[9F48]ICC Public Key Remainder',
    0x9F49 => '[9F49]Dynamic Data Authentication Data Object List (DDOL)',
    0x9F4A => '[9F4A]Static Data Authentication Tag List',
    0x9F4B => '[9F4B]Signed Dynamic Application Data',
    0x9F4D => '[9F4D]Log Entry',
    0x9F4E => '[9F4E]Merchant Name and Location',
    0x9F4F => '[9F4F]Log Format',
    0x9F51 => '[9F51]Application Currency Code',
    0x9F52 => '[9F52]Application Default Action (ADA)',
    0x9F53 => '[9F53]Consecutive Transaction Limit, International',
    0x9F54 => '[9F54]Cumulative Total Transaction Amount Limit',
    0x9F56 => '[9F56]Issuer Authentication Indicator',
    0x9F55 => '[9F55]Geographic Indicator',
    0x9F57 => '[9F57]Issuer Country Code',
    0x9F58 => '[9F58]Lower Consecutive Offline Limit (Card Velocity Checking)',
    0x9F59 => '[9F59]Upper Consecutive Offline Limit (Card Velocity Checking)',
    0x9F5C => '[9F5C]Cumulative Total Transaction Amount Upper Limit',
    0x9F5D => '[9F5D]Available Offline Spending Amount',
    0x9F5E => '[9F5E]Consecutive Transaction International Upper Limit',
    0x9F61 => '[9F61]Cardholder Certificate',
    0x9F62 => '[9F62]Cardholder Certificate Type',
    0x9f73 => '[9F73]Currency Conversion Factor',
    0x9F74 => '[9F74]ECC Issuer Authorization Code',
    0x9F75 => '[9F75]Cumulative Total Transaction Amount Limit (Dual Currency)',
    0x9F77 => '[9F77]ECC Funds Limit',
    0x9F78 => '[9F78]ECC Single Transaction Limit',
    0x9F79 => '[9F79]ECC Available Funds',
    0xBF0C => '[BF0C]FCI Issuer Discretionary Data'
    }
    AIP = [ [ "RFU (b8)",
              "SDA supported (b7)",
              "DDA supported (b6)",
              "Cardholder verification is supported (b5)",
              "Terminal risk management is to be performed (b4)",
              "Issuer authentication is supported (b3)",
              "RFU (b2)",
              "CDA supported (b1)"],
            [ "RFU (b8)",
              "RFU (b7)",
              "RFU (b6)",
              "RFU (b5)",
              "RFU (b4)",
              "RFU (b3)",
              "RFU (b2)",
              "RFU (b1)" ] ];
    def display_aip aip
      2.times{ |j|
        b = aip.byteAt(j);
        puts "  Byte #{j+1}"
        8.times{ |i|
          bit = 0x80 >> i;
          if  (b & bit) == bit
            puts "    #{AIP[j][i]}" 
          end
        }
      }
    end
end
  
class PBOC
  include EMV
  
  @@PSE = ByteString.new("1PAY.SYS.DDF01", ASCII)
  @@PPSE = ByteString.new("2PAY.SYS.DDF01", ASCII)
  @@FCI = 0x6F
  @@DFNAME = 0x84
  @@FCI_ISSUER = 0xA5
  @@SFI = 0x88
  @@TEMPLATE = 0x70
  @@AID = 0x4F
  @@LABEL = 0x50
  @@PRIORITY = 0x87
  @@RMTF1 = 0x80
  @@RMTF2 = 0x77
  @@AIP = 0x82
  @@AFL = 0x94
  @@CDOL1 = 0x8C
  @@CDOL2 = 0x8D
  @@RMTF2 = 0x77
  @@RMTF1 = 0x80
  
  @@TVR = 0x95
  
  @@TC = 0x40
  @@AAC = 0x00
  @@ARQC = 0x80
  attr_accessor :card_tags_heap,:terminal_tags_heap,:auth_data,:terminal_blacklist,:current_trans_demand_online
  def initialize card,crypto
    @current_trans_demand_online = false
    @terminal_blacklist = Array.new
    @card = card
    @crypto = crypto
    @pse = []
    @card_tags_heap = {}
    @terminal_tags_heap = { 0x95 => ByteString.new('0000000000',HEX),
                            'TSI' => ByteString.new('0000',HEX),
                            0x9F33 => ByteString.new("2028C0", HEX),  #Terminal Capability
                            0x9F35 => 22,    #terminal type => off-line with on-line capability
                            0x9F1B => 1000,  #Terminal Floor Limit
                            'MTP' => 0.5,    #Maximum Target Percentage 
                            'STV' => 200,    #Selection Threshold Value
                            'RTP' => 0.2     #Random Selection Target Percentage 
                          }
    @ca_public_key_hash = {}
    @auth_data = ByteString.new('',HEX)
  end
  
  def self.ARQC
    return @@ARQC
  end
  
  def self.TC
    return @@TC
  end
  
  def self.AAC
    return @@AAC
  end
  #
  #
  #
  def select dfname,first=true
    @card.select dfname,first
  end
  
  #
  #
  #
  def read_record sfi,record_no
    @card.read_record sfi,record_no
  end
  
  #
  #
  #
  def select_pse is_contact
    dfname = (is_contact ? @@PSE : @@PPSE)
    puts ">> Select PSE #{dfname.toString(ASCII)}"
    fci,sw = select dfname
    if sw != ByteString.new('9000',HEX)
       puts ">> Select PSE:No " + dfname.toString(ASCII) + " found" if sw != ByteString.new('6A82',HEX)
       puts ">> Select PSE:Card Locked" if sw != ByteString.new('6A81',HEX)
       return
    end
    
    tlvlist = TLVList.new(fci, TLV.EMV)
    tlv = tlvlist.index(0)
    assert(tlv.getTag() == @@FCI)
    tlvlist = TLVList.new(tlv.getValue(),TLV.EMV)
    assert(tlvlist.length >= 2)
    
    #Decode DF Name
    tlv = tlvlist.index(0);
    assert(tlv.getTag() == @@DFNAME)
    assert(tlv.getValue() == dfname)
    #Decode FCI Proprietary Template
    tlv = tlvlist.index(1);
    assert(tlv.getTag() == @@FCI_ISSUER)
    tlvlist = TLVList.new(tlv.getValue(), TLV.EMV)
    
    if is_contact
       # Decode SFI of the Directory Elementary File
       tlv = tlvlist.index(0);
       assert(tlv.getTag() == @@SFI);
       sfi = tlv.getValue()
       assert(sfi.length == 1)
       sfi = sfi.byteAt(0)
       # Read all records from Directory Elementary File
       recno = 1
       until (response_and_sw = read_record(sfi, recno))[1] != ByteString.new('9000',HEX) do
          recno+=1
          tlvlist = TLVList.new(response_and_sw[0], TLV.EMV)
          assert(tlvlist.length == 1);
          
          tlv = tlvlist.index(0);
          assert(tlv.getTag() == @@TEMPLATE)
          
          tlvlist = TLVList.new(tlv.getValue(), TLV.EMV)
          assert(tlvlist.length >= 1);
          
          tlvlist.entries.each{ |tlv_61|
              assert(tlv_61.getTag() == 0x61)
              @pse << TLVList.new(tlv_61.getValue(), TLV.EMV)
          }           
       end
    else #contactless
       tlv = tlvlist.index(0);
       assert(tlv.getTag() == 0xBF0C );
       tlvlist = TLVList.new(tlv.getValue(), TLV.EMV);
       
       tlvlist.entries.each{ |tlv_61|
          assert(tlv_61.getTag() == 0x61)
          @pse << TLVList.new(tlv_61.getValue(), TLV.EMV)
       }       
    end
    
    puts ">> AID list:"
    @pse.each{ |tlv_61|
      aid = tlv_61.find(@@AID).getValue()
      puts ">> #{aid}"
    }
    @pse
  end
  
  #
  #
  #
  def get_aid
      priority = 0xFFFF;
      aid = nil;
      i = 0
      while i < @pse.length
        tlv_4f = @pse[i].find(@@AID)
        assert( tlv_4f != nil)
        aid_entry = tlv_4f.getValue()
        
        tlv_50 = @pse[i].find(@@LABEL)
        assert( tlv_50 != nil)
        
        entry_priority = 0xFFFE;
        tlv_87 = @pse[i].find(@@PRIORITY) 
        
        if tlv_87
          entry_priority = tlv_87.getValue().toUnsigned()
          entry_priority &=0x0F
        end
        
        if entry_priority < priority
          prio = entry_priority
          aid = aid_entry
        end
        i += 1
      end
      @card_tags_heap[@@AID] = aid
      return aid   
  end
  
  #
  #
  #
  def select_aid aid
      puts ">> Select Application #{aid.toString()}"
      fci,sw = select aid
      assert( sw == ByteString.new('9000',HEX) )
      decode_fci(fci)  
      @card_tags_heap[@@AID] = aid
  end
  
  #
  #
  #
  def decode_fci fci
      tlvlist = TLVList.new(fci, TLV.EMV)
      tlv = tlvlist.index(0)
      assert(tlv.getTag() == @@FCI)
      tlvlist = TLVList.new(tlv.getValue(),TLV.EMV)
      assert(tlvlist.length >= 2)
      tlv = tlvlist.find(0xA5)
      assert(tlv != nil)
      tlvlist = TLVList.new(tlv.getValue(),TLV.EMV)
      
      tlvlist.entries.each{ |tlv|
          puts ">> Decode FCI: #{TAGS[tlv.getTag()]}  #{tlv.getValue()}"
          @card_tags_heap[tlv.getTag()] = tlv.getValue()
      }
  end  
  
  #
  #
  #
  def create_dol dol
    dol_data = ByteString.new("",HEX)
    tag = nil
    length = nil
    while dol.length > 0 do
      b = dol.byteAt(0)
      if (b&0x1F)==0x1F
          tag = dol.left(2)
          length = dol.byteAt(2);
          dol = dol.bytes(3)
      else
          tag = dol.left(1)
          length = dol.byteAt(1);
          dol = dol.bytes(2)
      end
      
      add_dol_data = @terminal_tags_heap[tag.toUnsigned()]
      
      if !add_dol_data.nil?
          assert(add_dol_data.length == length)
          dol_data = dol_data.concat(add_dol_data) 
      else
          raise StandardError.new("No teminal value for DOL Tag= %s" %tag.toString(HEX) )
      end
    end
    return dol_data
  end
  
  #
  #
  #
  def init_app_processing dol_data
    puts ">> Initiate Application Processing"
    dol_data = ByteString.new("83", HEX).concat(dol_data.getLV(TLV.EMV))
    response,sw = @card.get_processing_options dol_data
    assert( sw == ByteString.new('9000',HEX) )
    tlv_list = TLVList.new(response, TLV.EMV);
    assert(tlv_list.length == 1);
    tlv = tlv_list.index(0)
    #Format 1
    if tlv.getTag() == @@RMTF1 
        @card_tags_heap[@@AIP] = tlv.getValue().left(2);
        @card_tags_heap[@@AFL] = tlv.getValue().bytes(2);
        puts ">> AIP: #{TAGS[@@AIP]}  #{@card_tags_heap[@@AIP]}"
        display_aip @card_tags_heap[@@AIP]
        puts ">> AFL: #{TAGS[@@AFL]}  #{@card_tags_heap[@@AFL]}"
    else 
        assert(tlv.getTag() == @@RMTF2)
        tlv_list = TLVList.new(tlv.getValue(), TLV.EMV);
        assert(tlv_list.length >= 2);
      
        tlv_list.entries.each{ |tlv|
            @card_tags_heap[tlv.getTag()] = tlv.getValue()
            puts ">> Initiate Application Processing: #{TAGS[tlv.getTag()]}  #{tlv.getValue()}"
            display_aip @card_tags_heap[@@AIP] if tlv.getTag()==@@AIP
        }
    end
  end
  
  #
  #
  #
  def read_app_data_from_afl
    @auth_data = ByteString.new('',HEX)
    afl = @card_tags_heap[@@AFL]
    assert(!afl.nil?)
    #Must be a multiple of 4
    assert((afl.length & 0x03) == 0)
    afl.getBytes.each_slice(4){ |e|
        sfi = (e[0]>> 3)      #Short file identifier
        start_rec = e[1]      #Start record
        end_rec = e[2]        #End record
        data_auth_rec = e[3]  #Number of records included in data authentication 
        (start_rec .. end_rec).each { |n|
            data,sw = read_record(sfi,n)
            assert(sw == ByteString.new('9000',HEX),'Failed read application data from AFL')
            if data.length > 0
                 #Decode template
                 tlv_list = TLVList.new(data, TLV.EMV)
                 assert(tlv_list.length == 1)
                 tlv = tlv_list.index(0)
                 assert(tlv.getTag() == @@TEMPLATE)
                 
                 #authentication data     
                 if data_auth_rec > 0
                    if sfi <= 10 
                       @auth_data = @auth_data.concat(tlv.getValue())
                    else
                       @auth_data.concat(data)
                    end
                    data_auth_rec = data_auth_rec - 1
                 end
        
                 tlv_list = TLVList.new(tlv.getValue(), TLV.EMV)
                 tlv_list.entries.each{ |tlv|
                     @card_tags_heap[tlv.getTag()] = tlv.getValue()
                      puts ">> Read Application Data: #{TAGS[tlv.getTag()]}"
                      puts ">> #{tlv.getValue()}"
                 }
            end
        }    
    }
  end
  
  #
  # 
  #
  def add_ca_public_key rid,index,key
    if @ca_public_key_hash[rid.toString(HEX)].nil?
      @ca_public_key_hash[rid.toString(HEX)] = {}
    end
    @ca_public_key_hash[rid.toString(HEX)][index] = key
  end
  
  #
  #
  #
  def get_public_key_index
      index = @card_tags_heap[0x8F];
      return index.toUnsigned();    
  end
  
  #
  #
  #
  def get_rid
      aid = @card_tags_heap[@@AID]
      rid = aid.left(5)
  end
  
  #
  #
  #
  def get_ca_public_key
      @ca_public_key_hash[get_rid.toString(HEX)][get_public_key_index]
  end
  
  #
  # DDA
  #
  def dynamic_data_authentication icc_pk,qPBOC = false
    begin
      if @card_tags_heap[@@AIP].byteAt(0)&0x20 == 0x20
        signed_dynamic_app_data = nil
        if qPBOC
          random = @terminal_tags_heap[0x9F37]
          signed_dynamic_app_data = @card_tags_heap[0x9F4B]
        else
          random = @crypto.generateRandom(4)
          response,sw = @card.internal_authenticate random
          tlv_list = TLVList.new(response,TLV.EMV)
          assert(tlv_list.length == 1);
          tlv = tlv_list.index(0)
          
          if tlv.getTag == 0x80    #Format 1,see EMV_4.2_book_3 section6.5.9.4
            signed_dynamic_app_data = tlv.getValue
          elsif tlv.getTag == 0x77 #Format 2,see EMV_4.2_book_3 section6.5.9.4
            tlv_list = TLVList.new(tlv.getValue,TLV.EMV)
            tlv = tlv_list.find(0x9F4B)
            signed_dynamic_app_data = tlv.getValue 
          end
        end
        decrypted_sdad = @crypto.decrypt(icc_pk, Crypto.RSA, signed_dynamic_app_data)
        #1.If the Signed Dynamic Application Data has a length different from 
        #  the length of the ICC Public Key Modulus, DDA has failed
        icc_pk_modulus = icc_pk.getComponent(Key.MODULUS)
        assert(signed_dynamic_app_data.length == icc_pk_modulus.length,"DDA:Signed Dynamic Application Data and ICC Public Key Modulus should have the same length")
        #2.If the Recovered Data Trailer is not equal to 'BC', DDA has failed
        assert(decrypted_sdad.byteAt(decrypted_sdad.length - 1) == 0xBC,"DDA:The Recovered Data Trailer != 0xBC")
        #3.Check the Recovered Data Header. If it is not '6A', DDA has failed
        assert(decrypted_sdad.byteAt(0) == 0x6A,"DDA:The Recovered Data Header != 0x6A")
        #4.Check the Signed Data Format. If it is not '05', DDA has failed.
        assert(decrypted_sdad.byteAt(1) == 0x05,"DDA:The Signed Data Format != 0x05")
        #5.Concatenate from left to right the second to the sixth data elements in Table 17 
        #  (that is, Signed Data Format through Pad Pattern), followed by the data elements 
        #  specified by the DDOL.
        ldd = decrypted_sdad.byteAt(3);
        data_for_digest = decrypted_sdad.bytes(1, 3 + ldd + decrypted_sdad.length - ldd - 25)
        data_for_digest = data_for_digest.concat(random)
        #6.Apply the indicated hash algorithm (derived from the Hash Algorithm Indicator) 
        #  to the result of the concatenation of the previous step to produce the hash result.
        hash_concat = @crypto.digest(Crypto.SHA_1, data_for_digest)
        #7.Compare the calculated hash result from the previous step with the recovered Hash Result. 
        #  If they are not the same, DDA has failed.
        hash_decrypted_sdad = decrypted_sdad.bytes(decrypted_sdad.length - 21, 20);
        assert(hash_concat == hash_decrypted_sdad,"DDA:Recovered hash != generated hash")
        @card_tags_heap[0x9F4C] = ByteString.new(decrypted_sdad.bytes(1, 3 + ldd + decrypted_sdad.length - ldd - 25).toString(HEX).gsub(/(bb)*$/,''),HEX)
        set_bit_for_terminal_tag 'TSI',1,8
      end
    rescue AssertionFailure => e
      puts e.message
      set_bit_for_terminal_tag(@@TVR,1,4)
    end
  end
  #
  #see PBOC2.0_part_7 5.2.4 签名的静态应用数据验证
  #see EMV_4.2_book_2 Security and Key Management 5.4 Verification of Signed Static Application Data
  #
  def verify_signed_static_app_data issuer_pk
    begin
      if @card_tags_heap[@@AIP].byteAt(0)&0x40 == 0x40
        signed_static_app_data = @card_tags_heap[0x93]
        issuer_pk_modulus = issuer_pk.getComponent(Key.MODULUS)
        #1.If the Signed Static Application Data has a length different from 
        #  the length of the Issuer Public Key Modulus, SDA has failed.
        assert(signed_static_app_data.length == issuer_pk_modulus.length,"SDA:Signed Static Application Data and Issuer Public Key Modulus should have the same length")
        #2.If the Recovered Data Trailer is not equal to 'BC', SDA has failed.
        decrypted_ssad = @crypto.decrypt(issuer_pk, Crypto.RSA, signed_static_app_data)
        assert( decrypted_ssad.byteAt(decrypted_ssad.length-1)==0xBC,"SDA:The Recovered Data Trailer != 0xBC")
        #3.Check the Recovered Data Header. If it is not '6A', SDA has failed.
        assert(decrypted_ssad.byteAt(0) == 0x6A,"SDA:The Recovered Data Header != 0x6A")
        #4.Check the Signed Data Format. If it is not '03', SDA has failed.
        assert(decrypted_ssad.byteAt(1) == 0x03,"SDA:The Signed Data Format != 0x03")
        #5.Concatenate from left to right the second to the fifth data elements in Table 7
        #  (that is, Signed Data Format through Pad Pattern), followed by the static data 
        #  to be authenticated as specified in section 10.3 of Book 3. If the Static Data 
        #  Authentication Tag List is present and contains tags other than '82', then SDA has failed.
        data_for_digest = decrypted_ssad.bytes(1, (decrypted_ssad.length - 22))
        data_for_digest = data_for_digest.concat(@auth_data);
        sda_tag_list = @card_tags_heap[0x9F4A]
        value = @card_tags_heap[sda_tag_list.toUnsigned] if sda_tag_list
        data_for_digest = data_for_digest.concat(value)
        #6.Apply the indicated hash algorithm to the result of the concatenation of 
        #  the previous step to produce the hash result
        hash_concat = @crypto.digest(Crypto.SHA_1, data_for_digest)
        #7.Compare the calculated hash result from the previous step with 
        #  the recovered Hash Result. If they are not the same, SDA has failed
        hash_ssad  = decrypted_ssad.bytes(decrypted_ssad.length - 21, 20)
        assert(hash_concat == hash_ssad,"SDA:Generated hash != recovered hash")
        
        @card_tags_heap[0x9F45] = decrypted_ssad.bytes(3, 2)
        
        set_bit_for_terminal_tag 'TSI',1,8
      end
    rescue AssertionFailure => e
      puts e.message   
      set_bit_for_terminal_tag(@@TVR,1,7)
    end
  end
  
  #
  #
  #
  def set_bit_for_terminal_tag tag,byte_pos,bit_pos
    byte = @terminal_tags_heap[tag].getBytes[byte_pos-1] | (0x01 << (bit_pos-1))
    @terminal_tags_heap[tag].getBytes[byte_pos-1] = byte
  end
  #
  #see PBOC2.0_part_7 5.3.4 IC卡公钥的获取
  #see EMV_4.2_book_2 Security and Key Management 6.4 Retrieval of ICC Public Key
  #
  def recover_icc_public_key issuer_pk
    begin
      icc_cert = @card_tags_heap[0x9F46]
      issuer_pk_modulus = issuer_pk.getComponent(Key.MODULUS)
      decrypted_cert = @crypto.decrypt(issuer_pk, Crypto.RSA, icc_cert)
      #1.If the ICC Public Key Certificate has a length different from the length of
      #  the Issuer Public Key Modulus, offline dynamic data authentication has failed.
      assert(decrypted_cert.length == issuer_pk_modulus.length,"Recover ICC Public Key:ICC Public Key Certificate and Issuer Public Key Modulus should have the different length")
      #2.If the Recovered Data Trailer is not equal to 'BC', offline dynamic 
      #  data authentication has failed.
      assert(decrypted_cert.byteAt(issuer_pk_modulus.length - 1) == 0xBC,"Recover ICC Public Key:The Recovered Data Trailer != 0xBC")
      #3.Check the Recovered Data Header. If it is not '6A', offline dynamic 
      #  data authentication has failed.
      assert(decrypted_cert.byteAt(0) == 0x6A,"Recover ICC Public Key:The Recovered Data Header != 0x6A");
      #4.Check the Certificate Format. If it is not '04', offline dynamic data
      #  authentication has failed.
      assert(decrypted_cert.byteAt(1) == 0x04,"Recover ICC Public Key:The Certificate Format != 0x04");
      #5.Concatenate from left to right the second to the tenth data elements in Table 14 
      #  (that is, Certificate Format through ICC Public Key or Leftmost Digits of the ICC Public Key),
      #  followed by the ICC Public Key Remainder (if present), the ICC Public Key Exponent, 
      #  and finally the static data to be authenticated.If the Static Data Authentication Tag List
      #  is present and contains tags other than '82', then offline dynamic data authentication 
      #  has failed.
      data_for_digest = decrypted_cert.bytes(1, (decrypted_cert.length - 22));
      icc_pk_remainder = @card_tags_heap[0x9F48]
      icc_pk_exponent = @card_tags_heap[0x9F47]
      data_for_digest = data_for_digest.concat(icc_pk_remainder) unless icc_pk_remainder.nil?
      data_for_digest = data_for_digest.concat(icc_pk_exponent)
      
      data_for_digest = data_for_digest.concat(@auth_data);
      sda_tag_list = @card_tags_heap[0x9F4A]
      value = @card_tags_heap[sda_tag_list.toUnsigned] if sda_tag_list
      data_for_digest = data_for_digest.concat(value)
      #6.Apply the indicated hash algorithm (derived from the Hash Algorithm Indicator) 
      #  to the result of the concatenation of the previous step to produce the hash result.
      hash_concat = @crypto.digest(Crypto.SHA_1, data_for_digest)
      #7.Compare the calculated hash result from the previous step with the recovered Hash Result.
      #  If they are not the same, offline dynamic data authentication has failed.
      hash_icc  = decrypted_cert.bytes(decrypted_cert.length - 21, 20);
      assert(hash_icc == hash_concat,"Recover ICC Public Key:Recovered hash != generated hash")
      #8.Compare the recovered PAN to the Application PAN read from the ICC. If they are \
      #  not the same, offline dynamic data authentication has failed.
      pan_from_cert = decrypted_cert.bytes(2, 10).toString(HEX).gsub(/f/,'')
      pan = @card_tags_heap[0x5A].toString(HEX)[0...pan_from_cert.length]
      assert(pan_from_cert == pan,"Recover ICC Public Key:Issuer Identifier doesn't match the lefmost 3-8 PAN digits")
      #9.Verify that the last day of the month specified in the Certificate Expiration Date 
      #  is equal to or later than today‘s date. If not, offline dynamic data authentication 
      #  has failed.
      cert_exp_date = Date.strptime('28' + decrypted_cert.bytes(12, 2).toString(HEX),'%d%m%y')
      today = Date.today
      assert(today < cert_exp_date,"Recover ICC Public Key:The Certificate expired")
      #10.If the ICC Public Key Algorithm Indicator is not recognised, offline dynamic data 
      #  authentication has failed.
      pk_alg_indicator = decrypted_cert.byteAt(18);
      #11.If all the checks above are correct, concatenate the Leftmost Digits of 
      #  the ICC Public Key and the ICC Public Key Remainder (if present) to obtain 
      #  the ICC Public Key Modulus, and continue with the actual offline dynamic 
      #  data authentication described in the two sections below.
      left_most_digits = ByteString.new(decrypted_cert.bytes(21, (issuer_pk_modulus.length - 42)).toString(HEX).gsub(/(bb)*$/,''),HEX)      
      icc_pk_modulus = icc_pk_remainder.nil?  ? left_most_digits : left_most_digits.concat(icc_pk_remainder)
      icc_pk = Key.new()
      icc_pk.setComponent(Key.MODULUS, icc_pk_modulus)
      icc_pk.setComponent(Key.EXPONENT, icc_pk_exponent)      
      return icc_pk   
    rescue AssertionFailure => e
      puts e.message   
      return nil
    end
  end  
  #
  # see PBOC2.0_part_7 5.2.3 发卡行公钥获取
  # see EMV_4.2_book_2 Security and Key Management 5.3 Retrieval of Issuer Public Key
  #
  def recover_issuer_public_key
    begin
      certificate = @card_tags_heap[0x90]
      ca_pk = get_ca_public_key
      ca_pk_modulus = ca_pk.getComponent(Key.MODULUS)
      decrypted_cert = @crypto.decrypt(ca_pk, Crypto.RSA, certificate)
      #1.If the Issuer Public Key Certificate has a length different 
      #  from the length of the Certification Authority Public Key 
      #  Modulus,SDA has failed
      assert(decrypted_cert.length == ca_pk_modulus.length,"Recover Issuer Public Key:Issuer Public Key Certificate and Certification Authority Public Key Modulus should have the same length");
      #2.If the Recovered Data Trailer is not equal to 'BC', SDA has failed
      assert(decrypted_cert.byteAt(ca_pk_modulus.length - 1) == 0xBC,"Recover Issuer Public Key:The Recovered Data Trailer != 0xBC")
      #3.Check the Recovered Data Header. If it is not '6A', SDA has failed.
      assert(decrypted_cert.byteAt(0) == 0x6A,'Recover Issuer Public Key:The Recovered Data Header != 0x6A')
      #4.Check the Certificate Format. If it is not '02', SDA has failed.
      assert(decrypted_cert.byteAt(1) == 0x02,'Recover Issuer Public Key:The Certificate Format != 0x02');
      #5.Concatenate from left to right the second to the tenth data elements 
      #  in Table 6 (that is, Certificate Format through Issuer Public Key or 
      #  Leftmost Digits of the Issuer Public Key), followed by the Issuer 
      #  Public Key Remainder (if present), and finally the Issuer Public Key Exponen
      data_for_digest = decrypted_cert.bytes(1, 14 + (ca_pk_modulus.length - 36))
      issuer_pk_remainder = @card_tags_heap[0x92]
      issuer_pk_exponent = @card_tags_heap[0x9F32]
      data_for_digest = data_for_digest.concat(issuer_pk_remainder) unless issuer_pk_remainder.nil?
      data_for_digest = data_for_digest.concat(issuer_pk_exponent)
      #6.Apply the indicated hash algorithm to the result of the concatenation
      #  of the previous step to produce the hash result.
      hash_concat = @crypto.digest(Crypto.SHA_1, data_for_digest)
      #7.Compare the calculated hash result from the previous step with the 
      #  recovered Hash Result. If they are not the same, SDA has failed.
      hash_cert = decrypted_cert.bytes(15 + (ca_pk_modulus.length - 36), 20)
      assert(hash_cert == hash_concat,'Recover Issuer Public Key:Recovered hash != generated hash')
      #8.Verify that the Issuer Identifier matches the leftmost 3-8 PAN digits
      #  (allowing for the possible padding of the Issuer Identifier with hexadecimal
      #  'F's). If not, SDA has failed.
      pan_from_cert = decrypted_cert.bytes(2, 4).toString(HEX).gsub(/f/,'')
      pan = @card_tags_heap[0x5A].left(4).toString(HEX)[0...pan_from_cert.length]
      assert(pan_from_cert == pan,"Recover ICC Public Key:Issuer Identifier doesn't match the lefmost 3-8 PAN digits")
      #9.Verify that the last day of the month specified in the Certificate Expiration 
      #  Date is equal to or later than today‘s date. If the Certificate Expiration Date 
      #  is earlier than today‘s date, the certificate has expired, in which case SDA has failed.
      cert_exp_date = Date.strptime('28' + decrypted_cert.bytes(6, 2).toString(HEX),'%d%m%y')
      today = Date.today
      assert(today < cert_exp_date,'Recover ICC Public Key:The Certificate expired')
      #10.(optional step)Verify that the concatenation of RID, Certification Authority Public Key Index, 
      #   and Certificate Serial Number is valid. If not, SDA has failed.
      
      #11.If the Issuer Public Key Algorithm Indicator is not recognised, SDA has failed.
      
      #12.If all the checks above are correct, concatenate the Leftmost Digits of 
      #   the Issuer Public Key and the Issuer Public Key Remainder (if present)
      #   to obtain the Issuer Public Key Modulus, and continue with the next steps 
      #   for the verification of the Signed Static Application Data.
      left_most_digits = ByteString.new( decrypted_cert.bytes(15, (ca_pk_modulus.length - 36)).toString(HEX).gsub(/(bb)*$/,''),HEX) 
      issuer_pk_modulus = issuer_pk_remainder.nil?  ? left_most_digits : left_most_digits.concat(issuer_pk_remainder)
      
      issuer_pk = Key.new()
      issuer_pk.setComponent(Key.MODULUS, issuer_pk_modulus)
      issuer_pk.setComponent(Key.EXPONENT, issuer_pk_exponent)
      
      return issuer_pk      
    rescue AssertionFailure => e
      puts e.message
      return nil
    end
  end
  
  #
  #Terminal risk management is that portion of risk management performed by 
  #the terminal to protect the acquirer, issuer, and system from fraud.
  #
  def terminal_risk_management
    #PBOC2.0_books_6 7.8.4
    check_exception_file
    #PBOC2.0_books_6 7.8.5
    is_merchant_demand_online?
    #PBOC2.0_books_6 7.8.6 do not support terminal transaction log
    check_floor_limits
    #PBOC2.0_books_6 7.8.7
    random_transaction_selection
    #PBOC2.0_books_6 7.8.8
    velocity_checking
    #PBOC2.0_books_6 7.8.9
    is_new_card?
  end
  
  #
  #
  #
  def check_exception_file
    pan = @card_tags_heap[0x5A].toString(HEX).sub!(/(\d*)(f)/,'\1')
    if @terminal_blacklist.include? pan
      puts ">> Terminal Risk Management:#{pan} in the Terminal"
      set_bit_for_terminal_tag @@TVR,1,5
    end
  end
  
  #
  #
  #
  def is_merchant_demand_online?
    if @current_trans_demand_online
      puts ">> Terminal Risk Management:Merchant demands online."
      set_bit_for_terminal_tag @@TVR,4,4
    end
  end
  #
  #
  #
  def check_floor_limits
    terminal_floor_limit = @terminal_tags_heap[0x9F1B]
    if terminal_floor_limit
      authorized_amount =  @terminal_tags_heap[0x9F02].toString(HEX).to_i(10)
      if authorized_amount > terminal_floor_limit
        puts ">> Terminal Risk Management:Transaction exceeds terminal floor limit."
        set_bit_for_terminal_tag @@TVR,4,8
      end 
    end
  end 
  
  #
  #
  #
  def random_transaction_selection
    terminal_rand = rand
    authorized_amount =  @terminal_tags_heap[0x9F02].toString(HEX).to_i(10)
    if authorized_amount < @terminal_tags_heap['STV'] && terminal_rand <= @terminal_tags_heap['RTP']
      puts ">> Terminal Risk Management:Authorized amount[#{authorized_amount}] below the threshold [#{@terminal_tags_heap['STV']}] and with a random"
      puts ">> transaction number[#{terminal_rand}] smaller than or equal to the target percentage[#{@terminal_tags_heap['RTP']}]"
      puts ">> the terminal selects the transaction for on-line authorization by the issuer"
      set_bit_for_terminal_tag @@TVR,4,5
    elsif authorized_amount > @terminal_tags_heap['STV'] && authorized_amount < @terminal_tags_heap[0x9F1B]
      trans_target_percentage = @terminal_tags_heap['RTP']+(@terminal_tags_heap['MTP'] - @terminal_tags_heap['RTP'])*( (authorized_amount - @terminal_tags_heap['STV'])/(@terminal_tags_heap[0x9F1B] - @terminal_tags_heap['STV']))
      if terminal_rand < trans_target_percentage
        puts ">> Terminal Risk Management:Authorized amount[#{authorized_amount}] greater than or equal to the threshold value"
        puts ">> and smaller than the Terminal Floor Limit[#{@terminal_tags_heap[0x9F1B]}] and with a random[#{terminal_rand}] smaller than " 
        puts ">> or equal to the computed transaction target percentage[#{trans_target_percentage}], the terminal selects the transaction " 
        puts ">> for on-line authorization by the issuer."
        set_bit_for_terminal_tag @@TVR,4,5
      end
    end
  end
   
   
  #
  #
  #
  def velocity_checking
    lcol = @card_tags_heap['9F14']
    ucol = @card_tags_heap['9F23']
    if lcol && ucol
      atc = @card.get_data 0x9F,0x36
      last_atc = @card.get_data 0x9F,0x13
      if atc && last_atc && (atc.toUnsigned - last_atc.toUnsigned > lcol)
        puts ">> Terminal Risk Management:Lower Consecutive Off-line Limit exceeded."
        set_bit_for_terminal_tag @@TVR,4,7 
      end
      if atc && last_atc && (atc.toUnsigned - last_atc.toUnsigned > ucol)
        puts ">> Terminal Risk Management:Upper Consecutive Off-line Limit exceeded."
        set_bit_for_terminal_tag @@TVR,4,6 
      end
    end 
  end 
  
  
  #
  #
  #
  def is_new_card?
    last_atc = @card.get_data 0x9F,0x13
    if last_atc && (last_atc.toUnsigned == 0)
      puts ">> Terminal Risk Management:New card."
      set_bit_for_terminal_tag @@TVR,2,4 
    end
  end 
  
  #
  #Cardholder verification is performed to ensure that the person presenting 
  #the ICC is the person to whom the application in the card was issued.
  #
  def cardholder_verification    
    if @card_tags_heap[@@AIP].byteAt(0)&0x20 == 0x20
      cvm_8e = @card_tags_heap[0x8E]
      amount_x = cvm_8e.bytes(0,4)
      amount_y = cvm_8e.bytes(4,4)
      cvm_list = cvm_8e.bytes(8)
      cvm_result = nil
      assert(cvm_list.length%2 == 0,'Invalid CVM list')
      
      cvm_type_hash = { 0 => 'Fail CVM processing',
                        1 => 'Plaintext PIN verification performed by ICC',
                        2 => 'Enciphered PIN verified online',
                        3 => 'Plaintext PIN verification performed by ICC and signature',
                        4 => 'Enciphered PIN verification performed by ICC',
                        5 => 'Enciphered PIN verification performed by ICC and signature',
                     0x1E => 'Signature',
                     0x1F => 'No CVM required' }
      cvm_condition_hash = { 0 => 'Always',
                             1 => 'If unattended cash',
                             2 => 'If not unattended cash and not manual cash and not purchase with cashback',
                             3 => 'If terminal supports the CVM',
                             4 => 'If manual cash',
                             5 => 'If purchase with cashback',
                             6 => 'If transaction is in the application currency and is under X value',
                             7 => 'If transaction is in the application currency and is over X value',
                             8 => 'If transaction is in the application currency and is under Y value',
                             9 => 'If transaction is in the application currency and is over Y value',
                            10 => 'RFU',
                            11 => 'Reserved for use by individual payment systems' }
      next_cvm_hash = { true  => 'Apply succeeding CV Rule if this CVM is unsucccessful',
                        false => 'Fail cardholder verification if this CVM is unsuccessful' } 
     
      cvm_list.getBytes.each_slice(2){ |cvm|
        next_cvm = (cvm[0]&0x40 == 0x40)
        cvm_type = (cvm[0] & 0x3F)
        cvm_condition = cvm[1]
        puts '>> ' + cvm_condition_hash[cvm_condition] + ' {'
        puts '>>  %s'  %cvm_type_hash[cvm_type]
        puts '>> }'
        if cvm_condition == 0
          do_select_cvm cvm_type
        elsif cvm_condition == 1
          
        elsif cvm_condition == 2
        
        elsif cvm_condition == 3
          #check terminal capability if support 
          if @terminal_tags_heap[0x9F33].byteAt(1)&0x20 == 0x20
            puts ">> Terminal supports the CVM"
            cvm_result = do_select_cvm cvm_type
          end
          
          if cvm_result == :success
            puts ">> The above CVM success"
            set_bit_for_terminal_tag 'TSI',1,7
            break
          else
            puts ">> " + next_cvm_hash[next_cvm]
            if next_cvm
              next
            else
              set_bit_for_terminal_tag 'TSI',1,7
              set_bit_for_terminal_tag @@TVR,3,8  
            end
          end
          
        elsif cvm_condition == 4
  
        elsif cvm_condition == 5
          
        elsif cvm_condition == 6
        
        elsif cvm_condition == 7    
            
        elsif cvm_condition == 8
          
        elsif cvm_condition == 9
          
        elsif cvm_condition == 10
          
        elsif cvm_condition == 11
          
        end
      }
    end
  end
  
  #
  #
  #
  def do_select_cvm cvm_type
    case cvm_type
    when 0 
      return :fail
    when 1
      return icc_plaintext_pin_verification
    when 2
      return online_pin_verification
    when 3
      return icc_plaintext_pin_verification
    when 4
      return icc_enciphered_pin_verification
    when 5
      return icc_enciphered_pin_verification
    when 0x1E
      return :success
    when 0x1F
      return :fail
    end
  end 
  
  #
  #
  #
  def online_pin_verification
    #TODO
    return :success
  end
  #
  #
  #
  def icc_plaintext_pin_verification
    #TODO
    return :success
  end
  
  #
  #
  #
  def icc_enciphered_pin_verification
    #TODO
    return :success
  end
  
  #
  #Processing Restrictions function is to determine the degree of compatibility of the application 
  #in the terminal with the application in the ICC and to make any necessary adjustments, 
  #including possible rejection of the transaction.The terminal shall always execute this function.
  #
  def processing_restrictions
    check_app_version_num
    check_app_usage_control
    check_app_effective_date
    check_app_expiration_date
  end
  
  #
  #
  #
  def check_app_version_num
    begin
      app_version = @card_tags_heap[0x9F08]
      terminal_version = terminal_tags_heap[0x9F09].nil?  ? app_version : terminal_tags_heap[0x9F09]
      assert(app_version == terminal_version,'Processing Restrictions:card application version != terminal version')
    rescue AssertionFailure => e
      puts e.message
      set_bit_for_terminal_tag @@TVR,2,8   
    end
  end
  
  #
  #
  #
  def check_app_usage_control is_atm=false
    begin
      app_usage_control = @card_tags_heap[0x9F07]
      # default transaction type is 00(Goods and service)
      # 01 => Cash
      tran_type = @terminal_tags_heap[0x9C] ? @terminal_tags_heap[0x9C] : ByteString.new('00',HEX)
      issuer_country_code = @card_tags_heap[0x5F28]
      terminal_country_code = @terminal_tags_heap[0x9F1A] ? @terminal_tags_heap[0x9F1A] : issuer_country_code 
      if issuer_country_code == terminal_country_code
        case tran_type.toString(HEX)        
        when '00'
          #byte_1_bit_4 Valid for domestic services[国内服务有效]
          assert(app_usage_control.byteAt(0)&0x08 == 0x08,'Processing Restrictions:Invalid for domestic services')
          #byte_1_bit_6 Valid for domestic goods[国内商品有效]
          assert(app_usage_control.byteAt(0)&0x20 == 0x20,'Processing Restrictions:Invalid for domestic goods')
        when '01'
          #byte_1_bit_8 Valid for domestic cash transactions[国内现金交易有效]
          assert(app_usage_control.byteAt(0)&0x80 == 0x80,'Processing Restrictions:Invalid for domestic cash transactions')
        end
        
        #byte_2_bit_8 Domestic cashback allowed[允许国内返现]
        assert(app_usage_control.byteAt(0)&0x80 == 0x80,'Processing Restrictions:Domestic cashback forbidden') if @card_tags_heap[0x9F03]
      else
        case tran_type.toString(HEX)        
        when '00'
          #byte_1_bit_3 Valid for international services[国际服务有效]
          assert(app_usage_control.byteAt(0)&0x04 == 0x04,'Processing Restrictions:Invalid for international services')
          #byte_1_bit_5 Valid for international goods[国际商品有效]
          assert(app_usage_control.byteAt(0)&0x10 == 0x10,'Processing Restrictions:Invalid for international goods')
        when '01'
          #byte_1_bit_7 Valid for international cash transactions[国际现金交易有效]
          assert(app_usage_control.byteAt(0)&0x80 == 0x80,'Processing Restrictions:Invalid for international cash transactions')
        end
        #byte_2_bit_7 International cashback allowed[允许国际返现]
        assert(app_usage_control.byteAt(0)&0x40 == 0x40,'Processing Restrictions:International cashback forbidden') if @card_tags_heap[0x9F03]      
      end
      
      if is_atm
        #bit_2 Valid at ATMs[ATM 有效]
        assert(app_usage_control.byteAt(0)&0x02 == 0x02,'Processing Restrictions:Invid at ATMs')
      else
        #bit_1 Valid at terminals other than ATMs[除ATM 外的终端有效]
        assert(app_usage_control.byteAt(0)&0x01 == 0x01,'Processing Restrictions:Invalid at terminals other than ATMs')
      end
    rescue AssertionFailure => e
      puts e.message
      set_bit_for_terminal_tag @@TVR,2,5   
    end
  end
  
  #
  #
  def check_app_effective_date
    begin 
      effective_date = Date.strptime(@card_tags_heap[0x5F25].toString(HEX),'%y%m%d')
      today = Date.today
      assert(effective_date <= today, 'Processing Restrictions:Application not yet effective')
    rescue AssertionFailure => e
      puts e.message
      set_bit_for_terminal_tag @@TVR,2,6   
    end
  end 

  #
  #
  #
  def check_app_expiration_date
    begin 
      expiration_date = Date.strptime(@card_tags_heap[0x5F24].toString(HEX),'%y%m%d')
      today = Date.today
      assert(expiration_date >= today, 'Processing Restrictions:Expired application')
    rescue AssertionFailure => e
      puts e.message
      set_bit_for_terminal_tag @@TVR,2,7   
    end
  end 
  
  #
  #
  #
  def terminal_action_analysis
    ac_type = decide_ac_type
    generate_ac ac_type,1
  end
  
  #
  #
  #
  def generate_ac ac_type, times=1
    cdol = @card_tags_heap[@@CDOL1] if times == 1
    cdol = @card_tags_heap[@@CDOL2] if times == 2
    cdol_data = create_dol cdol
    ac,sw = @card.generate_ac ac_type,cdol_data
    
    tlv_list = TLVList.new(ac, TLV.EMV)
    assert(tlv_list.length == 1)
    tlv = tlv_list.index(0);
    if tlv.getTag == @@RMTF1    #Format 1 tag 80
      #Cryptogram Information Data (CID)
      @card_tags_heap[0x9F27] = tlv.getValue().bytes(0,1)
      #ATC
      @card_tags_heap[0x9F36] = tlv.getValue().bytes(1,2)
      #Application Cryptogram 
      @card_tags_heap[0x9F26] = tlv.getValue().bytes(3,8)
      @terminal_tags_heap['ARQC'] = @card_tags_heap[0x9F26] if ac_type == @@ARQC  
      #Issuer Application Data
      @card_tags_heap[0x9F10] = tlv.getValue().bytes(11)
      
      @card_tags_heap['CVR'] = @card_tags_heap[0x9F10].bytes(3,4)
      
    elsif tlv.getTag == @@RMTF2 #format 2 tag 77
      tlv_list = TLVList.new(tlv.getValue(), TLV.EMV)
      assert(tlv_list.length >= 2,"Template 77 should have more than 2 TLVs")
      tlv_list.entries.each{ |tlv|
        @card_tags_heap[tlv.getTag()] = tlv.getValue()
      }
    end
  end
  
  #
  #
  #
  def decide_ac_type
    @card_tags_heap[0x9F0E] ||= ByteString.new('0000000000',HEX)
    @card_tags_heap[0x9F0F] ||= ByteString.new('FFFFFFFFFF',HEX)
    @card_tags_heap[0x9F0D] ||= ByteString.new('FFFFFFFFFF',HEX)
    
    @terminal_tags_heap['TACDenial'] ||= ByteString.new('0000000000',HEX)
    @terminal_tags_heap['TACOnline'] ||= ByteString.new('0000000000',HEX)
    @terminal_tags_heap['TACDefault'] ||= ByteString.new('0000000000',HEX)
    
    terminal_type = @terminal_tags_heap[0x9F35]
    if @card_tags_heap[0x9F0E].and(@terminal_tags_heap[@@TVR]) != ByteString.new('0000000000',HEX) || \
       @terminal_tags_heap['TACDenial'].and(@terminal_tags_heap[@@TVR]) != ByteString.new('0000000000',HEX)
      puts ">> Terminal Action Analysis:Off-line declined [Z1]"
      @terminal_tags_heap[0x8A] = ByteString.new('Z1',ASCII)
      return @@AAC
    end
    #the terminal is "off-line-only" (with Terminal Type in the set of values 13, 23, 16, 26, 36)
    unless [13,23,16,26,36].include? terminal_type
      if @card_tags_heap[0x9F0F].and(@terminal_tags_heap[@@TVR]) != ByteString.new('0000000000',HEX) || \
         @terminal_tags_heap['TACOnline'].and(@terminal_tags_heap[@@TVR]) != ByteString.new('0000000000',HEX)
        puts ">> Terminal Action Analysis:requesting ARQC."
        return @@ARQC
      end
    end
    
    if [13,23,16,26,36].include? terminal_type
      if @card_tags_heap[0x9F0D].and(@terminal_tags_heap[@@TVR]) != ByteString.new('0000000000',HEX) || \
         @terminal_tags_heap['TACDefault'].and(@terminal_tags_heap[@@TVR]) != ByteString.new('0000000000',HEX)
        puts ">> Terminal Action Analysis:Off-line declined AAC."
        @terminal_tags_heap[0x8A] = ByteString.new('Z1',ASCII)
        return @@AAC
      end
    end
    puts ">> Terminal Action Analysis:Off-line approval."
    @terminal_tags_heap[0x8A] = ByteString.new('Y1',ASCII)   
    return @@TC
  end
  
  #
  #
  #
  def card_action_analysis
    parse_cvr
    advice = advice_message
    puts advice if advice
  end
  
  #
  #PBOC's CVR is different with EMV's CVR
  #
  def parse_cvr
    puts ">> Card Action Analysis:parse Card Verification Results(CVR)"
    cvr_byte_2 = @card_tags_heap['CVR'].byteAt(1)
    #bit 8-7
    case cvr_byte_2 >> 6
      when 0
        puts ">> CVR:[AAC] Returned in 2nd GENERATE AC"
      when 1
        puts ">> CVR:[TC] Returned in 2nd GENERATE AC"
      when 2
        puts ">> CVR:Second GENERATE AC Not Requested"
      when 3
        puts ">> CVR:RFU"
    end
    #bit 6-5
    case (cvr_byte_2&0x3F) >> 4
      when 0
        puts ">> CVR:[AAC] Returned in 1st GENERATE AC"
      when 1
        puts ">> CVR:[TC] Returned in 1st GENERATE AC"
      when 2
        puts ">> CVR:[ARQC] Returned in 1st GENERATE AC"
      when 3 
        puts ">> Bug"
    end
    #bit 4
    if cvr_byte_2 & 8 != 0
      puts ">> CVR:Issuer Authentication Failed"
    end
    #bit 3
    if cvr_byte_2 & 4 != 0
      puts ">> CVR:Offline PIN Verification Performed"
    end
    #bit 2
    if cvr_byte_2 & 2 != 0
      puts ">> CVR:Offline PIN Verification Performed and PIN Not Successfully Verified"
    end
    #bit 1
    if cvr_byte_2 & 1 != 0
      puts ">> CVR:Unable to go Online"
    end
    
    cvr_byte_3 = @card_tags_heap['CVR'].byteAt(2)
    #bit 8
    if cvr_byte_3 & 0x80 != 0
      puts ">> CVR:Last Online Transaction Not Completed"
    end
    #bit 7
    if cvr_byte_3 & 0x40 != 0
      puts ">> CVR:Online PIN Locked"
    end
    #bit 6
    if cvr_byte_3 & 0x20 != 0
      puts ">> CVR:Card Velocity Checking Failed"
    end
    #bit 5
    if cvr_byte_3 & 0x10 != 0
      puts ">> CVR:New Card"
    end
    #bit 4
    if cvr_byte_3 & 8 != 0
      puts ">> CVR:Issuer Authentication Failed on Previous Transaction"
    end    
    #bit 3
    if cvr_byte_3 & 4 != 0
      puts ">> CVR:Issuer Authentication Not Performed"
    end
    #bit 2
    if cvr_byte_3 & 2 != 0
      puts ">> CVR:Card Application locked Because of online PIN locked"
    end
    #bit 1
    if cvr_byte_3 & 1 != 0
      puts ">> CVR:AC since SDA failed on Previous Transaction"
    end
    
    cvr_byte_4 = @card_tags_heap['CVR'].byteAt(3)
    script_num = cvr_byte_4 >> 4
    if script_num > 0 
      puts "#{script_num} Successfully Processed Issuer Script Commands Containing Secure Messaging"                
    end
    
    #bit 4
    if cvr_byte_4 & 8 != 0
      puts ">> CVR:Issuer Script Failed on Previous Transaction"
    end    
    #bit 3
    if cvr_byte_4 & 4 != 0
      puts ">> CVR:AC since DDA failed on Previous Transaction"
    end
    #bit 2
    if cvr_byte_4 & 2 != 0
      puts ">> CVR:DDA Performed"
    end
    #bit 1
    if cvr_byte_4 & 1 != 0
      puts ">> CVR:RFU(0)"
    end
  end
  
  #
  #Cryptogram Information Data
  #b8 b7 b6 b5 b4 b3 b2 b1     Meaning
  #0  0                         AAC
  #0  1                         TC
  #1  0                         ARQC
  #1  1                         RFU
  #      x  x                   Payment System-specific cryptogram
  #            0                No advice required
  #            1                Advice required
  #               x  x  x       Reason/advice code
  #               0  0  0       No information given
  #               0  0  1       Service not allowed
  #               0  1  0       PIN Try Limit exceeded
  #               0  1  1       Issuer authentication failed
  #               1  x  x       Other values RFU
  #
  def analyze_ac_by_cid
    cid = @card_tags_heap[0x9F27]
    assert(cid != nil,"No Cryptogram Information Data")
    cid = cid.byteAt(0)>>6
    result = case cid
               when 0
                 @@AAC
               when 1
                 @@TC
               when 2
                 @@ARQC
             end
  end
  
  #
  #
  #
  def advice_message
    advice = nil
    cid = @card_tags_heap[0x9F27].byteAt(0)
    if cid & 0x08 == 0x08
      cid = cid << 5
      advice = case cid
                when 0x20
                  "Service not allowed"
                when 0x40
                  "PIN Try Limit exceeded"
                when 0x60
                  "Issuer authentication failed"
               end
    end
    advice 
  end
  
  #
  #The online processing function is performed when the terminal receives an ARQC 
  #in response to the first GENERATE AC command
  #
  def online_processing ac_key
    begin 
      if analyze_ac_by_cid == @@ARQC
        puts ">> Online Processing ..."
        #1.get session key
        ac_sk = get_session_key ac_key
        #2.card authentication
        puts "=========================="
        authenticate_card ac_sk
        puts "=========================="
        #3.get arpc and arc from Issuer
        iss_auth_data = fetch_issuer_authentication_data ac_sk
        #4.issuer external authenticate
        res,sw = issuer_authentication iss_auth_data
        assert(sw==ByteString.new('9000',HEX),"Issuer authentication failed")
        puts ">> Issuer authentication was successful"
      else
        puts ">> Do not support online processing"
      end
    rescue AssertionFailure => e
      puts e.message
    end
  end
  
  def get_udk mdk
    udk = Key.new()
    derived_pan = get_derived_pan  
    derived_pan = derived_pan.concat( derived_pan.xor(ByteString.new("FFFFFFFFFFFFFFFF", HEX)));
    @crypto.deriveKey(mdk, Crypto.DES_ECB, derived_pan, udk);
    return udk  
  end
  
  def get_session_key mdk,derived_time=2
    sk = Key.new()
    udk = Key.new()
    #ac udk
    if derived_time == 2
      udk = get_udk mdk
    else
      udk = mdk
    end
    
    #ac session key
    atc = @card_tags_heap[0x9F36]
    derived_atc = ByteString.new("000000000000", HEX).concat(atc)
    derived_atc = derived_atc.concat(ByteString.new("000000000000", HEX).concat( atc.xor( ByteString.new("FFFF", HEX))));
    @crypto.deriveKey(udk, Crypto.DES_ECB, derived_atc, sk);  
    return sk
  end
  #
  #Validate ARQC
  #
  def authenticate_card ac_sk 
    raw_data_for_arqc = @terminal_tags_heap[0x9F02]
    raw_data_for_arqc = raw_data_for_arqc.concat(@terminal_tags_heap[0x9F03])
    raw_data_for_arqc = raw_data_for_arqc.concat(@terminal_tags_heap[0x9F1A])
    raw_data_for_arqc = raw_data_for_arqc.concat(@terminal_tags_heap[0x95])
    raw_data_for_arqc = raw_data_for_arqc.concat(@terminal_tags_heap[0x5F2A])
    raw_data_for_arqc = raw_data_for_arqc.concat(@terminal_tags_heap[0x9A])
    raw_data_for_arqc = raw_data_for_arqc.concat(@terminal_tags_heap[0x9C])
    raw_data_for_arqc = raw_data_for_arqc.concat(@terminal_tags_heap[0x9F37])
    raw_data_for_arqc = raw_data_for_arqc.concat(@card_tags_heap[@@AIP])
    raw_data_for_arqc = raw_data_for_arqc.concat(@card_tags_heap[0x9F36])
    raw_data_for_arqc = raw_data_for_arqc.concat(@card_tags_heap[0x9F10].bytes(3,4))
    # retail MAC
    arqc  = @crypto.sign(ac_sk, Crypto.DES_MAC_EMV, raw_data_for_arqc)
    assert(arqc==@terminal_tags_heap['ARQC'],"Card ARQC != Issurer Calculated ARQC")
  end
  
  #
  #
  #
  def get_derived_pan
    derived_pan = @card_tags_heap[0x5A].toString(HEX)
    derived_pan.sub!(/(\d*)(f)/,'\2\1')    
    derived_pan = ByteString.new(derived_pan,HEX).concat(@card_tags_heap[0x5F34]).right(8)
  end
  #
  #Get ARPC and ARC from Issuer
  #
  def fetch_issuer_authentication_data ac_sk
    #Authorisation Response Code
    @terminal_tags_heap[0x8A] = ByteString.new("3030", HEX);
    orginal_arpc = @terminal_tags_heap['ARQC'].xor( @terminal_tags_heap[0x8A].concat(ByteString.new("000000000000", HEX)));
    arpc  = @crypto.encrypt(ac_sk, Crypto.DES_ECB, orginal_arpc);
    arpc = arpc.concat(@terminal_tags_heap[0x8A])
  end
  
  #
  #EXTERNAL AUTHENTICATE
  #
  def issuer_authentication iss_auth_data
    #check Issuer authentication is supported by ICC
    if @card_tags_heap[@@AIP].byteAt(0)&0x04 == 0x04
      puts ">> Issuer authentication is supported By ICC"
      @card.issuer_external_authenticate iss_auth_data
    end
  end
  
  #
  #EMV_4.2_book_3_p127 If the terminal decides to go online, completion shall be done 
  #when the second GENERATE AC command is issued.
  #
  #PBOC2.0_book_5_p54 只有在卡片行为分析后，卡片返回ARQC申请联机授权的情况下卡片执行结束操作
  #
  def completion
    arc = @terminal_tags_heap[0x8A].toString(ASCII) 
    if arc == '00' || arc =='01' || arc=='11'
      generate_ac(PBOC.TC,2)
    else
      generate_ac(PBOC.AAC,2)
    end
  end
  
  #
  #
  #
  def issuer_script_processing mac_key,script_type,options={},enc_key=nil
    script = nil
    
    mac_sk = get_session_key mac_key
    enc_udk = get_udk enc_key if enc_key
    enc_sk = get_session_key enc_udk,1 if enc_key
    
    case script_type
    when :put_data
      script = get_put_data_script mac_sk,options
    when :app_block
      script = get_simple_script mac_sk,{:command => "841E000004"}
    when :app_unblock
      script = get_simple_script mac_sk,{:command => "8418000004"}
    when :card_block
      script = get_simple_script mac_sk,{:command => "8416000004"}
    when :pin_change
      script = get_pin_change_script mac_sk,enc_udk,enc_sk,options
    when :pin_unblock
      script = get_simple_script mac_sk,{:command => "8424000004"}
    when :record_update
      script = get_record_update_script mac_sk,options
    end
    
    run_issuer_script script if script
  end
  
  #
  #PBOC2.0 UPDATE RECORD SCRIPT
  #
  def get_record_update_script mac_sk,options
    script = ByteString.new("9F1804",HEX).concat(ByteString.new("RECU",ASCII))
    p1 = options[:record_no]
    p2 = (options[:sfi] << 3) | 0x04
    ld = options[:record].length + 4
    record = ByteString.new(options[:record],HEX)
    command = ByteString.new("04 DC",HEX).concat(p1).concat(p2).concat(ld)
    
    script_for_mac = command.concat(@card_tags_heap[0x9F36])
    script_for_mac = script_for_mac.concat(@terminal_tags_heap['ARQC'])
    script_for_mac = script_for_mac.concat(record)
    mac = @crypto.sign(mac_sk, Crypto.DES_MAC_EMV, script_for_mac).left(4)
    command = command.concat(record).concat(mac)
    
    command_template = ByteString.new("86",HEX).concat(command.getLV(TLV.EMV))
    
    script = script.concat(command_template)
    script = ByteString.new("72",HEX).concat(script.getLV(TLV.EMV)) 
  end
  
  #
  # PBOC2.0 PIN CHANGE SCRIPT
  #
  def get_pin_change_script mac_sk,enc_udk,enc_sk,options
    script = ByteString.new("9F1804",HEX).concat(ByteString.new("PINC",ASCII))
    command = nil
    
    d1 = ByteString.new("00000000",HEX).concat(enc_udk.getComponent(Key.DES).bytes(4,4))
    d2 = ByteString.new(options[:new_pin],HEX)
    d3 = d1.xor(d2)
    d = nil
    if options[:old_pin]
      d = d3.xor(ByteString.new(options[:old_pin],HEX))
      d = ByteString.new("08",HEX).concat(d);
      command = ByteString.new("84 24 00 01 14",HEX)
    else
      d = ByteString.new("08",HEX).concat(d3);
      command = ByteString.new("84 24 00 02 14",HEX)
    end
    
    pin = @crypto.encrypt(enc_sk,Crypto.DES_ECB_P,d)
    script_for_mac = command.concat(@card_tags_heap[0x9F36])
    script_for_mac = script_for_mac.concat(@terminal_tags_heap['ARQC'])
    script_for_mac = script_for_mac.concat(pin)
    
    mac = @crypto.sign(mac_sk, Crypto.DES_MAC_EMV, script_for_mac).left(4)
    command = command.concat(pin).concat(mac)
    command_template = ByteString.new("86",HEX).concat(command.getLV(TLV.EMV))
    script = script.concat(command_template)
    script = ByteString.new("72",HEX).concat(script.getLV(TLV.EMV)) 
  end
  
  #
  #
  #
  def get_simple_script mac_sk,options
    script = ByteString.new("9F1804",HEX).concat(ByteString.new("NODS",ASCII))
    
    command = ByteString.new(options[:command],HEX)
    script_for_mac = command.concat(@card_tags_heap[0x9F36])
    script_for_mac = script_for_mac.concat(@terminal_tags_heap['ARQC'])
      
    mac = @crypto.sign(mac_sk, Crypto.DES_MAC_EMV, script_for_mac).left(4)
    command = command.concat(mac)
    command_template = ByteString.new("86",HEX).concat(command.getLV(TLV.EMV))
    script = script.concat(command_template)
    script = ByteString.new("72",HEX).concat(script.getLV(TLV.EMV)) 
  end
  
  #
  #
  #
  def get_put_data_script mac_sk,options
    command = Array.new
    options.each{|key,value|
      script_for_mac = ByteString.new("04DA", HEX)
      script_for_mac = script_for_mac.concat(ByteString.new(key,HEX))
      len = ("%02x" %(value.length/2+4))
      script_for_mac = script_for_mac.concat(ByteString.new(len,HEX))
      a_script = script_for_mac
      script_for_mac = script_for_mac.concat(@card_tags_heap[0x9F36])
      script_for_mac = script_for_mac.concat(@terminal_tags_heap['ARQC'])
      script_for_mac = script_for_mac.concat(ByteString.new(value,HEX))
      a_script = a_script.concat(ByteString.new(value,HEX))
      mac = @crypto.sign(mac_sk, Crypto.DES_MAC_EMV, script_for_mac).left(4)
      a_script = a_script.concat(mac)
      command << a_script
    } 
    script = ByteString.new("9F1804",HEX).concat(ByteString.new("PUTD",ASCII))
    command.each{ |c|
      command_template = ByteString.new("86",HEX).concat(c.getLV(TLV.EMV))
      script = script.concat(command_template)
    }
    script = ByteString.new("72",HEX).concat(script.getLV(TLV.EMV))
  end
  
  #
  #
  #
  def run_issuer_script script
    tlvlist = TLVList.new(script,TLV.EMV)
    assert(tlvlist.length == 1,"Only 71 Template or 72 Template")
    tlvlist = TLVList.new(tlvlist.index(0).getValue,TLV.EMV)
    tlvs_86 =  tlvlist.findAll(0x86)
    tlvs_86.each{ |tlv|
      @card.send tlv.getValue.getBytes
    }
  end
end